/*
 * Copyright (c) 2014, 2024, Marcus Hirt, Miroslav Wengner
 *
 * Robo4J is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Robo4J is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Robo4J. If not, see <http://www.gnu.org/licenses/>.
 */
package com.robo4j.socket.http.units;

import com.robo4j.ConfigurationException;
import com.robo4j.CriticalSectionTrait;
import com.robo4j.RoboContext;
import com.robo4j.RoboUnit;
import com.robo4j.configuration.Configuration;
import com.robo4j.socket.http.ProtocolType;
import com.robo4j.socket.http.channel.OutboundHttpSocketChannelHandler;
import com.robo4j.socket.http.enums.StatusCode;
import com.robo4j.socket.http.message.HttpDecoratedRequest;
import com.robo4j.socket.http.message.HttpDecoratedResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
import java.util.EnumSet;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import static com.robo4j.socket.http.util.RoboHttpUtils.HTTP_PROPERTY_PROTOCOL;
import static com.robo4j.socket.http.util.RoboHttpUtils.PROPERTY_BUFFER_CAPACITY;
import static com.robo4j.socket.http.util.RoboHttpUtils.PROPERTY_HOST;
import static com.robo4j.socket.http.util.RoboHttpUtils.PROPERTY_SOCKET_PORT;

/**
 * Http NIO Client for communication with external Robo4J units. Unit accepts
 *
 * @author Marcus Hirt (@hirt)
 * @author Miro Wengner (@miragemiko)
 * @see HttpDecoratedRequest type of message. Such message contains all
 * necessary information and HttpClientDecorator unit is only implementation
 * detail.
 */
@CriticalSectionTrait
public class HttpClientUnit extends RoboUnit<HttpDecoratedRequest> {
    private static final Logger LOGGER = LoggerFactory.getLogger(HttpClientUnit.class);
    private static final EnumSet<StatusCode> PROCESS_RESPONSES_STATUSES = EnumSet.of(StatusCode.OK,
            StatusCode.ACCEPTED);
    private volatile String host;
    private volatile Integer port;
    private Integer bufferCapacity;
    private ProtocolType protocol;
    private Lock lock = new ReentrantLock();

    public HttpClientUnit(RoboContext context, String id) {
        super(HttpDecoratedRequest.class, context, id);
    }

    @Override
    protected void onInitialization(Configuration configuration) throws ConfigurationException {
        bufferCapacity = configuration.getInteger(PROPERTY_BUFFER_CAPACITY, null);
        protocol = ProtocolType.valueOf(configuration.getString(HTTP_PROPERTY_PROTOCOL, "HTTP"));
        host = configuration.getString(PROPERTY_HOST, null);
        port = configuration.getInteger(PROPERTY_SOCKET_PORT, null);
        Objects.requireNonNull(host, "host required");
        if (port == null) {
            port = protocol.getPort();
        }
    }

    @Override
    public void onMessage(HttpDecoratedRequest message) {
        final HttpDecoratedRequest request = adjustRequest(message);
        if (message.getDenominator() == null) {
            LOGGER.info("reconfigured host: {}, port: {}", message.getHost(), message.getPort());
            return;
        }
        final InetSocketAddress address = new InetSocketAddress(request.getHost(), request.getPort());
        try (SocketChannel channel = SocketChannel.open(address)) {
            if (bufferCapacity != null) {
                channel.socket().setSendBufferSize(bufferCapacity);
            }

            final HttpDecoratedResponse decoratedResponse = processRequestByChannel(channel, request);

            if (decoratedResponse != null && PROCESS_RESPONSES_STATUSES.contains(decoratedResponse.getCode())) {
                if (!decoratedResponse.getCallbacks().isEmpty()) {
                    sendMessageToCallbacks(decoratedResponse.getCallbacks(), decoratedResponse.getMessage());
                }
            } else {
                LOGGER.warn("no callback or wrong response: {}", decoratedResponse);
            }

        } catch (Exception e) {
            LOGGER.error("not available: {}, no worry I continue sending. Error: {}", address, e.getMessage(), e);
        }
    }

    private HttpDecoratedResponse processRequestByChannel(SocketChannel byteChannel,
                                                          HttpDecoratedRequest message) {
        try (OutboundHttpSocketChannelHandler handler = new OutboundHttpSocketChannelHandler(byteChannel, message)) {
            handler.start();
            return handler.getDecoratedResponse();
        }
    }

    private HttpDecoratedRequest adjustRequest(HttpDecoratedRequest request) {
        lock.lock();
        try {
            if (request.getHost() != null && !request.getHost().isEmpty()) {
                host = request.getHost();
            }
            if (request.getPort() != null) {
                port = request.getPort();
            }
        } finally {
            lock.unlock();
        }
        request.setHost(host);
        request.setPort(port);
        request.addHostHeader();
        return request;
    }

    private void sendMessageToCallbacks(List<String> callbacks, Object message) {
        callbacks.forEach(callback -> sendMessageToCallback(callback, message));
    }

    private void sendMessageToCallback(String callback, Object message) {
        getContext().getReference(callback).sendMessage(message);
    }

}
